/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Validate } from '../helper/Validate';
import { Attribute } from './Attribute';
import List from '@ohos.util.List';
import { StringUtil } from '../internal/StringUtil';
import { OutputSettings } from './OutputSettings';
import { ParseSettings } from '../parser/ParseSettings';
import {JavaIterator} from '../util/JavaIterator'
import constants from '../util/Constants';

/**
 * The attributes of an Element.
 * <p>
 * Attributes are treated as a map: there can be only one value associated with an attribute key/name.
 * </p>
 * <p>
 * Attribute name and value comparisons are generally <b>case sensitive</b>. By default for HTML, attribute names are
 * normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by
 * name.
 * </p>
 */
export class Attributes /*implements IterableIterator<Attribute>*/ {

  /*private _pointer: number = 0;
  [Symbol.iterator](): IterableIterator<Attribute>{
    return this;
  }

  next(): IteratorResult<Attribute> {
        if (this._pointer < this._size) {
          // while (this._pointer < this._size) {
          //   if (this.isInternalKey(this.keys[this._pointer])) {
          //     // skip over internal keys
          //     this._pointer++
          //   } else {
          //     break
          //   }
          // }
          return {
            done: false,
            value: new Attribute(this.keys[this._pointer], this.vals[this._pointer++], this)
          };
        } else {
          return {
            done: true,
            value: null
          };
        }
      }

  hasNext():boolean {
    while (this._pointer < this._size) {
      if (this.isInternalKey(this.keys[this._pointer])) {
        // skip over internal keys
        this._pointer++
      } else {
        break
      }
    }
    return this._pointer < this._size
  }

  remove(){
    return this.removeAttribute(--this._pointer)
  }*/

  iterator():JavaIterator<Attribute>{
    return this[Symbol.iterator]()
  }

  // The Attributes object is only created on the first use of an attribute; the Element will just have a null
  // Attribute slot otherwise
  static readonly dataPrefix = "data-";
  // Indicates a jsoup internal key. Can't be set via HTML. (It could be set via accessor, but not too worried about
  // that. Suppressed from list, iter.
  static readonly InternalPrefix = '/';
  // sampling found mean count when attrs present = 1.49; 1.08 overall. 2.6:1 don't have any attrs.
  private static readonly _InitialCapacity = 3
  // manages the key/val arrays
  private static readonly GrowthFactor = 2;
  static readonly NotFound = -1;
  private static readonly _EmptyString = "";
  // the number of instance fields is kept as low as possible giving an object size of 24 bytes
  // number of slots used (not total capacity, which is keys.length)
  private _size = 0;
  keys: String[] = []
  // Genericish: all non-internal attribute values must be Strings and are cast on access.
  vals: any[] = []
  // check there's room for more
  private checkCapacity(minNewSize: number) {
    //自动扩容 这里不做处理
  }

  indexOfKey(key: string): number {
    Validate.notNull(key)
    for (let index = 0; index < this._size; index++) {
      if (key === this.keys[index]) {
        return index
      }
    }
    return Attributes.NotFound
  }

  private indexOfKeyIgnoreCase(key: string): number {
    Validate.notNull(key)
    for (let index = 0; index < this._size; index++) {
      if (key.toLowerCase() === this.keys[index].toLowerCase()) {
        return index
      }
    }
    return Attributes.NotFound
  }

  // we track boolean attributes as null in values - they're just keys. so returns empty for consumers
  // casts to String, so only for non-internal attributes
  static checkNotNull(val: any): string {
    if (val === undefined || val === null) {
      return  Attributes._EmptyString
    } else if (val instanceof String) {
      return val.toString()
    } else {
      return val as string
    }
  }

  /**
   Get an attribute value by key.
   @param key the (case-sensitive) attribute key
   @return the attribute value if set; or empty string if not set (or a boolean attribute).
   @see #hasKey(String)
   */
  public get(key: string): string {
    let i = this.indexOfKey(key)
    return i === Attributes.NotFound ? Attributes._EmptyString : Attributes.checkNotNull(this.vals[i])
  }

  /**
   * Get an attribute's value by case-insensitive key
   * @param key the attribute name
   * @return the first matching attribute value if set; or empty string if not set (ora boolean attribute).
   */
  public getIgnoreCase(key: string): string {
    let i = this.indexOfKeyIgnoreCase(key)
    return i === Attributes.NotFound ? Attributes._EmptyString : Attributes.checkNotNull(this.vals[i])
  }

  /**
   Get an arbitrary user data object by key.
   * @param key case sensitive key to the object.
   * @return the object associated to this key, or {@code null} if not found.
   */
  getUserData(key: string): any {
    Validate.notNull(key)
    if (!this.isInternalKey(key)) {
      key = constants.internalKey(key)
    }
    let i = this.indexOfKeyIgnoreCase(key)
    return i === Attributes.NotFound ? null : this.vals[i]
  }

  /**
   * Adds a new attribute. Will produce duplicates if the key already exists.
   * @see Attributes#put(String, String)
   */
  public add(key: string, value: string): Attributes {
    this.addObject(key, value)
    return this
  }

  private addObject(key: string, value: any) {
    this.checkCapacity(this._size + 1)
    this.keys[this._size] = key
    this.vals[this._size] = value
    this._size++
  }

  /**
   * Set a new attribute, or replace an existing one by key.
   * @param key case sensitive attribute key (not null)
   * @param value attribute value (may be null, to set a boolean attribute)
   * @return these attributes, for chaining
   */
  public put(key: string, value: string): Attributes {
    Validate.notNull(key)
    let i = this.indexOfKey(key)
    if (i !== Attributes.NotFound) {
      this.vals[i] = value
    } else {
      this.add(key, value)
    }
    return this
  }

  /**
   Put an arbitrary user-data object by key. Will be treated as an internal attribute, so will not be emitted in HTML.
   * @param key case sensitive key
   * @param value object value
   * @return these attributes
   * @see #getUserData(String)
   */
  putUserData(key: string, value: any): Attributes {
    Validate.notNull(key)
    if (!this.isInternalKey(key)) {
      key = constants.internalKey(key)
    }
    Validate.notNull(value)
    let i = this.indexOfKey(key)
    if (i !== Attributes.NotFound) {
      this.vals[i] = value
    } else {
      this.addObject(key, value)
    }
    return this
  }

  putIgnoreCase(key: string, value: string) {
    let i = this.indexOfKeyIgnoreCase(key)
    if (i !== Attributes.NotFound) {
      this.vals[i] = value
      // case changed, update
      if (this.keys[i] !== key) {
        this.keys[i] = key
      }
    } else {
      this.add(key, value)
    }
  }

  /**
   * Set a new boolean attribute, remove attribute if value is false.
   * @param key case <b>insensitive</b> attribute key
   * @param value attribute value
   * @return these attributes, for chaining
   */
  public putBoolean(key: string, value: boolean): Attributes {
    if (value) {
      this.putIgnoreCase(key, null)
    } else {
      this.remove(key)
    }
    return this
  }

  /**
   Set a new attribute, or replace an existing one by key.
   @param attribute attribute with case sensitive key
   @return these attributes, for chaining
   */
  public putAttributes(attribute: Attribute): Attributes {
    Validate.notNull(attribute)
    this.put(attribute.getKey(), attribute.getValue())
    attribute.parent = this
    return this
  }

  // removes and shifts up
  private removeByIndex(index: number) {
    Validate.isFalse(index >= this._size)
    let shifted = this._size - index - 1
    if (shifted > 0) {
      for (let i = 0; i < shifted; i++) {
        this.keys[index + i] = this.keys[index + i+ 1]
        this.vals[index + i] = this.vals[index + i+ 1]
      }
    }
    this._size--
    this.keys.pop()
    this.vals.pop()
  }

  /**
   Remove an attribute by key. <b>Case sensitive.</b>
   @param key attribute key to remove
   */
  public remove(key: string) {
    let i = this.indexOfKey(key)
    if (i !== Attributes.NotFound) {
      this.removeByIndex(i)
    }
  }

  /**
   Remove an attribute by key. <b>Case insensitive.</b>
   @param key attribute key to remove
   */
  removeIgnoreCase(key: string) {
    let i = this.indexOfKeyIgnoreCase(key)
    if (i !== Attributes.NotFound) {
      this.removeByIndex(i)
    }
  }

  /**
   Tests if these attributes contain an attribute with this key.
   @param key case-sensitive key to check for
   @return true if key exists, false otherwise
   */
  public hasKey(key: string): boolean {
    return this.indexOfKey(key) !== Attributes.NotFound
  }

  /**
   Tests if these attributes contain an attribute with this key.
   @param key key to check for
   @return true if key exists, false otherwise
   */
  public hasKeyIgnoreCase(key: string): boolean {
    return this.indexOfKeyIgnoreCase(key) !== Attributes.NotFound
  }

  /**
   * Check if these attributes contain an attribute with a value for this key.
   * @param key key to check for
   * @return true if key exists, and it has a value
   */
  public hasDeclaredValueForKey(key: string): boolean {
    let i = this.indexOfKey(key)
    return i !== Attributes.NotFound && this.vals[i] !== null && this.vals[i] !== undefined
  }

  /**
   * Check if these attributes contain an attribute with a value for this key.
   * @param key case-insensitive key to check for
   * @return true if key exists, and it has a value
   */
  public hasDeclaredValueForKeyIgnoreCase(key: string): boolean {
    let i = this.indexOfKeyIgnoreCase(key)
    return i !== Attributes.NotFound && this.vals[i] !== null && this.vals[i] !== undefined
  }

  /**
   Get the number of attributes in this set, including any jsoup internal-only attributes. Internal attributes are
   excluded from the {@link #html()}, {@link #asList()}, and {@link #iterator()} methods.
   @return size
   */
  public size(): number {
    return this._size
  }

  /**
   * Test if this Attributes list is empty (size==0).
   */
  public isEmpty(): boolean {
    return this._size === 0
  }

  /**
   Add all the attributes from the incoming set to this set.
   @param incoming attributes to add to these attributes.
   */
  public addAll(incoming: Attributes) {
    if (incoming.size() === 0) {
      return
    }
    this.checkCapacity(this._size + incoming.size())
    // if this set is empty, no need to check existing set, so can add() vs put()
    let needsPut = this._size !== 0
    // (and save bashing on the indexOfKey()
    for (let index = 0; index < incoming.size(); index++) {
      // let attr = incoming[index] as Attribute;
      if (needsPut) {
        // this.putAttributes(attr)
      } else {
        // this.add(attr.getKey(),attr.getValue())
      }
    }
  }

  public clearAttributes() {
    this._size = 0
    this.keys = []
    this.vals = []
  }

  // public getAttributes():Attribute[] {
  //   let result:Attribute[] = []
  //   let j=0
  //   for (let index = 0; index < this._size; index++) {
  //     if (this.isInternalKey(this.keys[index])) {
  //       continue
  //     }
  //     let attr = new Attribute(this.keys[index],this.vals[index] as string)
  //     result[j++] = attr
  //   }
  //   return result
  // }

  /**
   Get the attributes as a List, for iteration.
   @return a view of the attributes as an unmodifiable List.
   */
  public asList(): List<Attribute> {
    let list = new List<Attribute>()
    for (let index = 0; index < this._size; index++) {
      if (this.isInternalKey(this.keys[index].toString())) {
        continue
      }
      let attr = new Attribute(this.keys[index], this.vals[index] as string)
      list.add(attr)
    }
    return list
  }

  /**
   * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys
   * starting with {@code data-}.
   * @return map of custom data attributes.
   */
  dataset() {
    return new Dataset(this)
  }

  /**
   Get the HTML representation of these attributes.
   @return HTML
   */
  html(): string {
    let sb = StringUtil.borrowBuilder()
    try {
      // output settings a bit funky, but this html() seldom used
      //FIXME
      // this.html1(sb,(new Document("")).getOutputSettings())
      this.html1(sb, new OutputSettings())
    } catch (e) {
    }
    return StringUtil.releaseBuilder(sb)
  }

  html1(accum: string[], out: OutputSettings) {
    let sz = this._size
    for (let index = 0; index < sz; index++) {
      if (this.isInternalKey(this.keys[index])) {
        continue
      }
      let key = Attribute.getValidKey(this.keys[index].toString(), out.getSyntax())
      if (key !== undefined && key !== null) {
        accum.push(' ')
        Attribute.htmlNoValidate(key, this.vals[index] as string, accum, out)
      }
    }
  }

  public toString(): string {
    return this.html()
  }

  public equals(o: any): boolean {
    if (this === o) {
      return true
    }
    if (o === undefined || o === null) {
      return false
    }
    let that = o as Attributes
    if (this._size !== that.size()) {
      return false
    }
    for (let index = 0; index < this._size; index++) {
      let key = this.keys[index]
      let thatI = that.indexOfKey(key.toString())
      if (thatI === Attributes.NotFound) {
        return false
      }
      let val = this.vals[index]
      let thatVal = that.vals[thatI]
      if (val === undefined || val === null) {
        if (thatVal !== undefined && thatVal !== null) {
          return false
        }
      } else if (val !== thatVal) {
        return false
      }
    }
    return true
  }

  /**
   * Calculates the hashcode of these attributes, by iterating all attributes and summing their hashcodes.
   * @return calculated hashcode
   */
  public hashCode(): number { // note parent not considered
    let result = this._size
    result = 31 * result + this.keys[0].charCodeAt(0);
    result = 31 * result + this.vals[0].charCodeAt(0);
    return result;
  }

  public clone(): Attributes {
    try {
      let clone = new Attributes()
      clone = Object.assign(clone, this);
      clone._size = this._size
      for (let index = 0; index < this._size; index++) {
        clone.keys[index] = this.keys[index]
        clone.vals[index] = this.vals[index]
      }
      return clone
    } catch (e) {
    }
  }

  /**
   * Internal method. Lowercases all keys.
   */
  public normalize() {
    for (let index = 0; index < this._size; index++) {
      this.keys[index] = this.keys[index].toLowerCase()
    }
  }

  public deduplicate(settings: ParseSettings): number {
    if (this.isEmpty()) {
      return 0
    }
    let preserve = settings.preserveAttributeCase()
    let dupes = 0
    OUTER: for (let i = 0; i < this.keys.length; i++) {
      for (let j = i + 1; j < this.keys.length; j++) {
        if (this.keys[j] === undefined || this.keys[j] === null) {
          continue OUTER
        }
        if ((preserve && this.keys[i] === this.keys[j]) || (!preserve && this.keys[i].toLowerCase() === this.keys[j].toLowerCase())) {
          dupes++
          this.removeByIndex(j)
          j--
        }
      }
    }
    return dupes
  }




  private isInternalKey(key: string|String): boolean {
    return key !== undefined && key !== null && key.length > 1 && key.charAt(0) === constants.InternalPrefix;
  }

  // [Symbol.iterator](): IterableIterator<Attributes> {
  //   for (let index = 0; index < this._size; index++) {
  //     return new Attribute(this.keys[index],this.vals[index],this)
  //   }
  // }

  [Symbol.iterator](): JavaIterator<Attribute> {
    let index = 0;
    let attributesInstance = this;

    const iterator: JavaIterator<Attribute> = {
      next(): IteratorResult<Attribute> {
        while (index < attributesInstance._size) {
          if (attributesInstance.isInternalKey(attributesInstance.keys[index])) {
            // skip over internal keys
            index++
          } else {
            break
          }
        }
        if (index < attributesInstance._size) {
          return {
            done: false,
            value: new Attribute(attributesInstance.keys[index], attributesInstance.vals[index++], attributesInstance)
          };
        } else {
          return {
            done: true,
            value: null
          };
        }
      },
      hasNext(): boolean {
        while (index < attributesInstance._size) {
          if (attributesInstance.isInternalKey(attributesInstance.keys[index])) {
            // skip over internal keys
            index++
          } else {
            break
          }
        }
        return index < attributesInstance._size
      },
      remove() {
        attributesInstance.removeByIndex(--index)
      },
      [Symbol.iterator](): IterableIterator<Attribute> {
        return iterator;
      }
    }
    return iterator;
  }

  static dataKey(key: string): string {
    return `${Attributes.dataPrefix}${key}`
  }
}

export class Dataset extends Map<string, string> {
  private readonly _attributes: Attributes

  constructor(attributes: Attributes) {
    super()
    this._attributes = attributes
    for (let attribute of this._attributes) {
      if (attribute.isDataAttribute()) {
        this.set(attribute.getKey().substring(Attributes.dataPrefix.length), attribute.getValue())
      }
    }
  }

  public put(key: string, value: string): string {
    let dataKey = Attributes.dataKey(key)
    let oldValue = this._attributes.hasKey(dataKey) ? this._attributes.get(dataKey) : null
    this._attributes.put(dataKey, value)
    this.set(dataKey, value)
    return oldValue
  }
}

